アクションとショートカットの使用
このページでは、物理キーボード イベントをユーザーのアクションにバインドする方法について説明します。 インターフェース。たとえば、アプリケーションでキーボード ショートカットを定義するには、次のようにします。 ページはあなたのためのものです。
概要
GUI アプリケーションが何かを行うには、ユーザーが伝えたいアクションが必要です。 へのアプリケーションする何か。アクションは多くの場合、次のような単純な関数です。 アクション (値の設定やファイルの保存など) を直接実行します。もっと大きなところでは ただし、アクションを呼び出すためのコードなど、物事はより複雑です。 また、アクション自体のコードは別の場所にある必要がある場合があります。 ショートカット(キーバインド)は何も知らないレベルでの定義が必要かも 彼らが引き起こすアクションについて。
そこで Flutter のアクションとショートカット システムが役に立ちます。
開発者は、それにバインドされたインテントを満たすアクションを定義できます。この中で
コンテキストでは、インテントはユーザーが実行したい一般的なアクションであり、Intent
クラス インスタンスは、Flutter でこれらのユーザー インテントを表します。アンIntent
汎用的なものであり、さまざまな場所でさまざまなアクションによって実行されます。
コンテキスト。アンAction
単純なコールバックにすることもできます (次の場合のように)
のCallbackAction
) または全体と統合するより複雑なもの
元に戻す/やり直しアーキテクチャ (たとえば) またはその他のロジック。
Shortcuts
キーまたは組み合わせを押すことによってアクティブ化されるキー バインディングです。
キーの。キーの組み合わせは、バインドされたインテントとともにテーブル内に存在します。いつ
のShortcuts
ウィジェットがそれらを呼び出すと、一致するインテントが
フルフィルメントのためのアクションサブシステム。
アクションとショートカットの概念を説明するために、この記事では ユーザーが両方を使用してテキスト フィールド内のテキストを選択およびコピーできるシンプルなアプリ ボタンとショートカット。
なぜアクションとインテントを分離するのでしょうか?
なぜキーの組み合わせをアクションに直接マッピングしないのかと疑問に思うかもしれません。なぜ そもそも意図があるのか?分離しておくと便利だからです。 キー マッピング定義がどこにあるか (多くの場合は高レベル)、 アクション定義がどこにあるか (多くの場合は低レベル)、そしてそれは 単一のキーの組み合わせを目的のキーにマップできることが重要です。 アプリ内での操作を制御し、どのアクションにも自動的に適応させます の意図された操作を実行します。 焦点を当てたコンテキスト。
たとえば、Flutter にはActivateIntent
それぞれのタイプをマッピングするウィジェット
の対応するバージョンへのコントロールActivateAction
(そしてそれは実行されます
コントロールをアクティブにするコード)。このコードは多くの場合、かなりプライベートである必要があります
作業を行うためのアクセス権。追加の間接層が存在する場合、Intent
が提供する
存在しなかった場合は、アクションの定義を次のレベルまで引き上げる必要があります。
ここで、の定義インスタンスは、Shortcuts
ウィジェットがそれらを認識できるため、
どのアクションを実行すべきかについて必要以上の知識を得る近道
呼び出し、必ずしも持っているとは限らない状態にアクセスしたり提供したりする
またはそれ以外の必要があります。これにより、コードで 2 つの懸念事項をより明確に分離できるようになります。
独立。
インテントは、同じアクションが複数の用途に使用できるようにアクションを構成します。アン
この例はDirectionalFocusIntent
、移動する方向を指定します。
焦点を合わせて、DirectionalFocusAction
どちらの方向に進むべきかを知るために
フォーカスを移動します。注意してください: に状態を渡さないでください。Intent
それが当てはまる
のすべての呼び出しに対してAction
: そのような状態は に渡される必要があります。
のコンストラクタAction
それ自体、Intent
知る必要があることから
過度に。
なぜコールバックを使用しないのでしょうか?
また、なぜコールバックの代わりにコールバックを使用しないのかと疑問に思うかもしれません。Action
物体?主な理由は、アクションが、
を実装することで有効になりますisEnabled
。また、キーがあれば役立つことがよくあります。
バインディングとそれらのバインディングの実装は別の場所にあります。
柔軟性のないコールバックだけが必要な場合は、Actions
とShortcuts
を使用できます。CallbackShortcuts
ウィジェット:
@override
Widget build(BuildContext context) {
return CallbackShortcuts(
bindings: <ShortcutActivator, VoidCallback>{
const SingleActivator(LogicalKeyboardKey.arrowUp): () {
setState(() => count = count + 1);
},
const SingleActivator(LogicalKeyboardKey.arrowDown): () {
setState(() => count = count - 1);
},
},
child: Focus(
autofocus: true,
child: Column(
children: <Widget>[
const Text('Press the up arrow key to add to the counter'),
const Text('Press the down arrow key to subtract from the counter'),
Text('count: $count'),
],
),
),
);
}
ショートカット
以下でわかるように、アクションはそれ自体でも便利ですが、最も一般的な用途は
この場合、それらをキーボード ショートカットにバインドする必要があります。これが、Shortcuts
用のウィジェットです。
これは、ウィジェット階層に挿入され、キーの組み合わせを定義します。
そのキーの組み合わせが押されたときのユーザーの意図を表します。変換する
具体的なアクションへのキーの組み合わせの意図された目的、Actions
マッピングに使用されるウィジェットIntent
にAction
。たとえば、次のことができます
を定義するSelectAllIntent
、それを自分のものにバインドしますSelectAllAction
またはあなたのCanvasSelectAllAction
そして、その 1 つのキー バインディングから、システムは
アプリケーションのどの部分にフォーカスがあるかに応じて、どちらかになります。方法を見てみましょう
キーバインディング部分は機能します。
@override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyA):
SelectAllIntent(),
},
child: Actions(
dispatcher: LoggingActionDispatcher(),
actions: <Type, Action<Intent>>{
SelectAllIntent: SelectAllAction(model),
},
child: Builder(
builder: (context) => TextButton(
child: const Text('SELECT ALL'),
onPressed: Actions.handler<SelectAllIntent>(
context,
SelectAllIntent(),
),
),
),
),
);
}
人に渡された地図は、Shortcuts
ウィジェットマップLogicalKeySet
(またはShortcutActivator
、以下の注を参照)Intent
実例。論理キー
set は 1 つ以上のキーのセットを定義し、Intent は意図されたキーを示します。
キーを押す目的。のShortcuts
ウィジェットはマップ内のキー入力を検索します。
を見つけるためにIntent
インスタンス、アクションに与えますinvoke()
方法。
ショートカットマネージャー
ショートカット マネージャーは、Shortcuts
ウィジェット、パス
重要なイベントを受信したときにそれを実行します。方法を決定するためのロジックが含まれています。
キーを処理する、ツリーを上って他のショートカットを見つけるためのロジック
マッピングを作成し、キーの組み合わせとインテントのマップを維持します。
のデフォルトの動作ですが、ShortcutManager
通常は望ましいのですが、Shortcuts
ウィジェットはShortcutManager
サブクラス化してカスタマイズできること
その機能性。
たとえば、各キーをログに記録したい場合、Shortcuts
ウィジェットが処理され、
を作ることができますLoggingShortcutManager
:
class LoggingShortcutManager extends ShortcutManager {
@override
KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event) {
final KeyEventResult result = super.handleKeypress(context, event);
if (result == KeyEventResult.handled) {
print('Handled shortcut $event in $context');
}
return result;
}
}
さて、毎回、Shortcuts
ウィジェットはショートカットを処理し、キーを出力します
イベントと関連するコンテキスト。
行動
Actions
アプリケーションが実行できる操作の定義を可能にする
で呼び出して実行します。Intent
。アクションは有効または無効にすることができます。
それらを呼び出したインテント インスタンスを引数として受け取り、許可します。
意図による構成。
アクションの定義
アクションは、最も単純な形式では、次のサブクラスにすぎません。Action<Intent>
とinvoke()
方法。これは単に関数を呼び出す単純なアクションです。
提供されたモデル:
class SelectAllAction extends Action<SelectAllIntent> {
SelectAllAction(this.model);
final Model model;
@override
void invoke(covariant SelectAllIntent intent) => model.selectAll();
}
または、新しいクラスを作成するのが面倒な場合は、CallbackAction
:
CallbackAction(onInvoke: (intent) => model.selectAll());
アクションを作成したら、次のコマンドを使用してそれをアプリケーションに追加します。Actions
ウィジェット、地図を取得しますIntent
にタイプしますAction
s:
@override
Widget build(BuildContext context) {
return Actions(
actions: <Type, Action<Intent>>{
SelectAllIntent: SelectAllAction(model),
},
child: child,
);
}
のShortcuts
ウィジェットはFocus
ウィジェットのコンテキストとActions.invoke
に
どのアクションを呼び出すかを見つけます。もしShortcuts
ウィジェットに一致するものが見つかりません
最初のインテントタイプActions
ウィジェットが見つかると、次のウィジェットが考慮されます
祖先Actions
ウィジェットなど、ウィジェットのルートに到達するまで続きます。
ツリーを参照するか、一致するインテント タイプを見つけて、対応するアクションを呼び出します。
アクションの呼び出し
アクション システムには、アクションを呼び出す方法がいくつかあります。これまでで最も一般的なのは
方法は、Shortcuts
前のセクションで説明したウィジェット、
ただし、アクション サブシステムに問い合わせて、
アクション。キーにバインドされていないアクションを呼び出すことができます。
たとえば、インテントに関連付けられたアクションを見つけるには、次を使用できます。
Action<SelectAllIntent>? selectAll =
Actions.maybeFind<SelectAllIntent>(context);
これは、Action
に関連するSelectAllIntent
ある場合は入力します
指定された場所で利用可能context
。利用できない場合は null を返します。もしも
関連するAction
常に利用可能である必要があるので、使用してくださいfind
それ以外のmaybeFind
、一致するものが見つからない場合に例外をスローしますIntent
タイプ。
アクション (存在する場合) を呼び出すには、次のように呼び出します。
Object? result;
if (selectAll != null) {
result = Actions.of(context).invokeAction(selectAll, SelectAllIntent());
}
これを次の 1 つの呼び出しに結合します。
Object? result =
Actions.maybeInvoke<SelectAllIntent>(context, SelectAllIntent());
場合によっては、ボタンを押したり、
別のコントロール。これを行うには、Actions.handler
を作成する関数
インテントに有効なアクションへのマッピングがある場合はハンドラー クロージャを返し、それを返します。
そうでない場合は null 。一致するものがない場合はボタンが無効になります。
コンテキスト内で有効なアクション:
@override
Widget build(BuildContext context) {
return Actions(
actions: <Type, Action<Intent>>{
SelectAllIntent: SelectAllAction(model),
},
child: Builder(
builder: (context) => TextButton(
child: const Text('SELECT ALL'),
onPressed: Actions.handler<SelectAllIntent>(
context,
SelectAllIntent(controller: controller),
),
),
),
);
}
のActions
ウィジェットは次の場合にのみアクションを呼び出しますisEnabled(Intent intent)
true を返し、ディスパッチャがそれを考慮すべきかどうかをアクションが決定できるようにします。
呼び出しのために。アクションが有効になっていない場合、Actions
ウィジェットが与える
ウィジェット階層の上位にある別の有効なアクション (存在する場合)
実行する。
前の例では、Builder
なぜならActions.handler
とActions.invoke
(例) 提供されたアクションのみを検索しますcontext
、 と
例が合格した場合、context
に与えられたbuild
機能、フレームワーク
探し始めるその上現在のウィジェット。を使ってBuilder
を許可します
同じフレームワークで定義されたアクションを見つけるためのフレームワークbuild
関数。
アクションを必要とせずに呼び出すことができます。BuildContext
、しかし、以来Actions
ウィジェットには、呼び出す有効なアクションを見つけるためのコンテキストが必要です。
独自のものを作成するか、提供する必要がありますAction
インスタンス、または
適切なコンテキストでそれを見つけるActions.find
。
アクションを呼び出すには、アクションをinvoke
のメソッドActionDispatcher
、自分で作成したもの、または
既存 Actions
を使用したウィジェットActions.of(context)
方法。かどうか確かめる
アクションは呼び出す前に有効になっていますinvoke
。もちろんお電話だけでも可能ですinvoke
アクション自体に、Intent
, しかし、その後はどれもオプトアウトします
アクション ディスパッチャーが提供するサービス (ログ記録、元に戻す/やり直し、
すぐ)。
アクションディスパッチャ
ほとんどの場合、アクションを呼び出して、そのアクションを実行させたいだけです。 気にしないで。ただし、実行されたアクションをログに記録したい場合もあります。
ここでデフォルトを置き換えますActionDispatcher
カスタムディスパッチャを使用する
が入ってきます。ActionDispatcher
にActions
ウィジェットとそれ
あらゆるものからアクションを呼び出しますActions
を設定しないウィジェットの下にあるウィジェット
独自のディスパッチャ。
最初のものActions
アクションを呼び出すときに行うのは、ActionDispatcher
そして、呼び出しのためにアクションをそれに渡します。何もない場合は、
デフォルトが作成されますActionDispatcher
それは単にアクションを呼び出すだけです。
ただし、呼び出されたすべてのアクションのログが必要な場合は、独自のログを作成できます。LoggingActionDispatcher
仕事をするために:
class LoggingActionDispatcher extends ActionDispatcher {
@override
Object? invokeAction(
covariant Action<Intent> action,
covariant Intent intent, [
BuildContext? context,
]) {
print('Action invoked: $action($intent) from $context');
super.invokeAction(action, intent, context);
return null;
}
}
次に、それをトップレベルに渡しますActions
ウィジェット:
@override
Widget build(BuildContext context) {
return Actions(
dispatcher: LoggingActionDispatcher(),
actions: <Type, Action<Intent>>{
SelectAllIntent: SelectAllAction(model),
},
child: Builder(
builder: (context) => TextButton(
child: const Text('SELECT ALL'),
onPressed: Actions.handler<SelectAllIntent>(
context,
SelectAllIntent(),
),
),
),
);
}
これにより、次のように、実行されるすべてのアクションがログに記録されます。
flutter: Action invoked: SelectAllAction#906fc(SelectAllIntent#a98e3) from Builder(dependencies: _[ActionsMarker])
それを一緒に入れて
の組み合わせActions
とShortcuts
強力です: ジェネリックを定義できます
ウィジェット レベルで特定のアクションにマップされるインテント。シンプルなアプリはこちら
これは、上で説明した概念を示しています。アプリはテキストフィールドを作成します。
隣には「すべて選択」ボタンと「クリップボードにコピー」ボタンもあります。ボタン
仕事を達成するためにアクションを呼び出します。呼び出されたすべてのアクションと
ショートカットが記録されます。
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// A text field that also has buttons to select all the text and copy the
/// selected text to the clipboard.
class CopyableTextField extends StatefulWidget {
const CopyableTextField({super.key, required this.title});
final String title;
@override
State<CopyableTextField> createState() => _CopyableTextFieldState();
}
class _CopyableTextFieldState extends State<CopyableTextField> {
late TextEditingController controller = TextEditingController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Actions(
dispatcher: LoggingActionDispatcher(),
actions: <Type, Action<Intent>>{
ClearIntent: ClearAction(controller),
CopyIntent: CopyAction(controller),
SelectAllIntent: SelectAllAction(controller),
},
child: Builder(builder: (context) {
return Scaffold(
body: Center(
child: Row(
children: <Widget>[
const Spacer(),
Expanded(
child: TextField(controller: controller),
),
IconButton(
icon: const Icon(Icons.copy),
onPressed:
Actions.handler<CopyIntent>(context, const CopyIntent()),
),
IconButton(
icon: const Icon(Icons.select_all),
onPressed: Actions.handler<SelectAllIntent>(
context, const SelectAllIntent()),
),
const Spacer(),
],
),
),
);
}),
);
}
}
/// A ShortcutManager that logs all keys that it handles.
class LoggingShortcutManager extends ShortcutManager {
@override
KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event) {
final KeyEventResult result = super.handleKeypress(context, event);
if (result == KeyEventResult.handled) {
print('Handled shortcut $event in $context');
}
return result;
}
}
/// An ActionDispatcher that logs all the actions that it invokes.
class LoggingActionDispatcher extends ActionDispatcher {
@override
Object? invokeAction(
covariant Action<Intent> action,
covariant Intent intent, [
BuildContext? context,
]) {
print('Action invoked: $action($intent) from $context');
super.invokeAction(action, intent, context);
return null;
}
}
/// An intent that is bound to ClearAction in order to clear its
/// TextEditingController.
class ClearIntent extends Intent {
const ClearIntent();
}
/// An action that is bound to ClearIntent that clears its
/// TextEditingController.
class ClearAction extends Action<ClearIntent> {
ClearAction(this.controller);
final TextEditingController controller;
@override
Object? invoke(covariant ClearIntent intent) {
controller.clear();
return null;
}
}
/// An intent that is bound to CopyAction to copy from its
/// TextEditingController.
class CopyIntent extends Intent {
const CopyIntent();
}
/// An action that is bound to CopyIntent that copies the text in its
/// TextEditingController to the clipboard.
class CopyAction extends Action<CopyIntent> {
CopyAction(this.controller);
final TextEditingController controller;
@override
Object? invoke(covariant CopyIntent intent) {
final String selectedString = controller.text.substring(
controller.selection.baseOffset,
controller.selection.extentOffset,
);
Clipboard.setData(ClipboardData(text: selectedString));
return null;
}
}
/// An intent that is bound to SelectAllAction to select all the text in its
/// controller.
class SelectAllIntent extends Intent {
const SelectAllIntent();
}
/// An action that is bound to SelectAllAction that selects all text in its
/// TextEditingController.
class SelectAllAction extends Action<SelectAllIntent> {
SelectAllAction(this.controller);
final TextEditingController controller;
@override
Object? invoke(covariant SelectAllIntent intent) {
controller.selection = controller.selection.copyWith(
baseOffset: 0,
extentOffset: controller.text.length,
affinity: controller.selection.affinity,
);
return null;
}
}
/// The top level application class.
///
/// Shortcuts defined here are in effect for the whole app,
/// although different widgets may fulfill them differently.
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String title = 'Shortcuts and Actions Demo';
@override
Widget build(BuildContext context) {
return MaterialApp(
title: title,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.escape): const ClearIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyC):
const CopyIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyA):
const SelectAllIntent(),
},
child: const CopyableTextField(title: title),
),
);
}
}
void main() => runApp(const MyApp());